﻿
using System;
using System.Linq;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using static LightCAD.Three.Constants;
using static LightCAD.Three.TypeUtils;
using LightCAD.Three.OpenGL;

namespace LightCAD.Three
{
    public class WebGLState
    {
        public class Buffers
        {
            public ColorBuffer color;
            public DepthBuffer depth;
            public StencilBuffer stencil;
        }
        private readonly bool isWebGL2;
        private readonly WebGLExtensions extensions;
        private readonly WebGLCapabilities capabilities;
        public class ColorBuffer
        {
            public bool locked = false;
            public Vector4 color = new Vector4();
            public bool currentColorMask = false;
            public Vector4 currentColorClear = new Vector4(0, 0, 0, 0);
            public void setMask(bool colorMask)
            {

                if (currentColorMask != colorMask && !locked)
                {
                    gl.colorMask(colorMask, colorMask, colorMask, colorMask);
                    currentColorMask = colorMask;
                }
            }

            public void setLocked(bool _lock)
            {

                locked = _lock;

            }

            public void setClear(float r, float g, float b, float a, bool premultipliedAlpha = false)
            {

                if (premultipliedAlpha)
                {

                    r *= a; g *= a; b *= a;

                }

                color.set(r, g, b, a);

                if (currentColorClear.equals(color) == false)
                {
                    gl.clearColor(r, g, b, a);
                    currentColorClear.copy(color);

                }

            }

            public void reset()
            {

                locked = false;

                currentColorMask = false;
                currentColorClear.set(-1, 0, 0, 0); // set to invalid state

            }
        }
        public class DepthBuffer
        {
            private WebGLState state;
            public bool locked = false;
            public bool? currentDepthMask = false;
            public int? currentDepthFunc;
            public float? currentDepthClear = 0;
            public DepthBuffer(WebGLState state)
            {
                this.state = state;
            }
            public void setTest(bool depthTest)
            {

                if (depthTest)
                {

                    state.enable(gl.DEPTH_TEST);

                }
                else
                {

                    state.disable(gl.DEPTH_TEST);

                }

            }
            public void setMask(bool depthMask)
            {
                if (currentDepthMask != depthMask && !locked)
                {

                    gl.depthMask(depthMask);
                    currentDepthMask = depthMask;
                }
            }
            public void setFunc(int depthFunc)
            {
                if (currentDepthFunc != depthFunc)
                {
                    switch (depthFunc)
                    {

                        case NeverDepth:

                            gl.depthFunc(gl.NEVER);
                            break;

                        case AlwaysDepth:

                            gl.depthFunc(gl.ALWAYS);
                            break;

                        case LessDepth:

                            gl.depthFunc(gl.LESS);
                            break;

                        case LessEqualDepth:

                            gl.depthFunc(gl.LEQUAL);
                            break;

                        case EqualDepth:

                            gl.depthFunc(gl.EQUAL);
                            break;

                        case GreaterEqualDepth:

                            gl.depthFunc(gl.GEQUAL);
                            break;

                        case GreaterDepth:

                            gl.depthFunc(gl.GREATER);
                            break;

                        case NotEqualDepth:

                            gl.depthFunc(gl.NOTEQUAL);
                            break;

                        default:

                            gl.depthFunc(gl.LEQUAL);
                            break;
                    }
                    currentDepthFunc = depthFunc;
                }
            }
            public void setLocked(bool _lock)
            {

                locked = _lock;

            }
            public void setClear(float depth)
            {
                if (currentDepthClear != depth)
                {
                    gl.clearDepth(depth);
                    currentDepthClear = depth;
                }
            }
            public void reset()
            {

                locked = false;

                currentDepthMask = null;
                currentDepthFunc = null;
                currentDepthClear = null;

            }

        }
        public class StencilBuffer
        {
            private WebGLState state;
            public bool locked = false;
            public uint? currentStencilMask;
            public int? currentStencilFunc;
            public int? currentStencilRef;
            public uint? currentStencilFuncMask;
            public int? currentStencilFail;
            public int? currentStencilZFail;
            public int? currentStencilZPass;
            public int? currentStencilClear;
            public StencilBuffer(WebGLState state)
            {
                this.state = state;
            }
            public void setTest(bool stencilTest)
            {

                if (!locked)
                {

                    if (stencilTest)
                    {

                        state.enable(gl.STENCIL_TEST);

                    }
                    else
                    {

                        state.disable(gl.STENCIL_TEST);

                    }

                }

            }

            public void setMask(uint stencilMask)
            {

                if (currentStencilMask != stencilMask && !locked)
                {

                    gl.stencilMask(stencilMask);
                    currentStencilMask = stencilMask;

                }

            }

            public void setFunc(int stencilFunc, int stencilRef, uint stencilMask)
            {

                if (currentStencilFunc != stencilFunc ||
                     currentStencilRef != stencilRef ||
                     currentStencilFuncMask != stencilMask)
                {

                    gl.stencilFunc(stencilFunc, stencilRef, stencilMask);

                    currentStencilFunc = stencilFunc;
                    currentStencilRef = stencilRef;
                    currentStencilFuncMask = stencilMask;
                }

            }

            public void setOp(int stencilFail, int stencilZFail, int stencilZPass)
            {

                if (currentStencilFail != stencilFail ||
                     currentStencilZFail != stencilZFail ||
                     currentStencilZPass != stencilZPass)
                {

                    gl.stencilOp(stencilFail, stencilZFail, stencilZPass);

                    currentStencilFail = stencilFail;
                    currentStencilZFail = stencilZFail;
                    currentStencilZPass = stencilZPass;

                }

            }

            public void setLocked(bool _lock)
            {

                locked = _lock;

            }

            public void setClear(int stencil)
            {

                if (currentStencilClear != stencil)
                {

                    gl.clearStencil(stencil);
                    currentStencilClear = stencil;

                }

            }

            public void reset()
            {

                locked = false;

                currentStencilMask = null;
                currentStencilFunc = null;
                currentStencilRef = null;
                currentStencilFuncMask = null;
                currentStencilFail = null;
                currentStencilZFail = null;
                currentStencilZPass = null;
                currentStencilClear = null;

            }
        }
        private readonly ColorBuffer colorBuffer;
        private readonly DepthBuffer depthBuffer;
        private readonly StencilBuffer stencilBuffer;
        public readonly JsObj<int, int?> uboBindings;
        public readonly JsObj<int, JsObj<UniformsGroup, int?>> uboProgramMap;

        public JsObj<int, bool?> enabledCapabilities;
        public JsObj<int, GLInt> currentBoundFramebuffers;
        public JsObj<GLInt, JsArr<int>> currentDrawbuffers;
        public JsArr<int> defaultDrawbuffers;
        public int? currentProgram;
        public bool currentBlendingEnabled;
        public int? currentBlending;
        public int? currentBlendEquation;
        public int? currentBlendSrc;
        public int? currentBlendDst;
        public int? currentBlendEquationAlpha;
        public int? currentBlendSrcAlpha;
        public int? currentBlendDstAlpha;
        public bool? currentPremultipledAlpha;
        public bool? currentFlipSided;
        public int? currentCullFace;
        public float? currentLineWidth;
        public float? currentPolygonOffsetFactor;
        public float? currentPolygonOffsetUnits;
        public readonly int maxTextures;
        public bool lineWidthAvailable = false;
        public float version = 0;
        public readonly string glVersion;
        public int? currentTextureSlot;
        public JsObj<int, BoundTexture> currentBoundTextures;
        public readonly double[] scissorParam;
        public readonly double[] viewportParam;
        public readonly Vector4 currentScissor;
        public readonly Vector4 currentViewport;
        public readonly JsObj<int, GLInt> emptyTextures;
        public readonly JsObj<int, int?> equationToGL;
        public readonly JsObj<int, int?> factorToGL;
        public Buffers buffers;
        public WebGLState(WebGLExtensions extensions, WebGLCapabilities capabilities)
        {
            this.isWebGL2 = capabilities.isWebGL2;
            this.extensions = extensions;
            this.capabilities = capabilities;
            this.colorBuffer = new ColorBuffer();
            this.depthBuffer = new DepthBuffer(this);
            this.stencilBuffer = new StencilBuffer(this);
            this.uboBindings = new JsObj<int, int?>();
            this.uboProgramMap = new JsObj<int, JsObj<UniformsGroup, int?>>();
            this.enabledCapabilities = new JsObj<int, bool?>();

            this.currentBoundFramebuffers = new JsObj<int, GLInt>();
            this.currentDrawbuffers = new JsObj<GLInt, JsArr<int>>();
            this.defaultDrawbuffers = new JsArr<int>();

            this.currentProgram = -1;

            this.currentBlendingEnabled = false;
            this.currentBlending = null;
            this.currentBlendEquation = null;
            this.currentBlendSrc = null;
            this.currentBlendDst = null;
            this.currentBlendEquationAlpha = null;
            this.currentBlendSrcAlpha = null;
            this.currentBlendDstAlpha = null;
            this.currentPremultipledAlpha = false;

            this.currentFlipSided = null;
            this.currentCullFace = null;

            this.currentLineWidth = null;

            this.currentPolygonOffsetFactor = null;
            this.currentPolygonOffsetUnits = null;

            this.maxTextures = (int)gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);

            this.lineWidthAvailable = true;
            this.version = 0;
            this.glVersion = gl.GetString(gl.VERSION).ToString();
            if (glVersion.IndexOf("WebGL") != -1)
            {

                version = parseFloat(new Regex(@"^WebGL(\d)").Matches(glVersion)[1].Value);
                lineWidthAvailable = (version >= 1.0);

            }
            else if (glVersion.IndexOf("OpenGL ES") != -1)
            {

                version = parseFloat(new Regex(@"^OpenGL ES(\d)").Matches(glVersion)[1].Value);
                lineWidthAvailable = (version >= 2.0);

            }

            this.currentTextureSlot = -1;
            this.currentBoundTextures = new JsObj<int, BoundTexture>();
            this.scissorParam = ((int[])gl.getParameter(gl.SCISSOR_BOX)).Select(v => (double)v).ToArray();
            this.viewportParam = ((int[])gl.getParameter(gl.VIEWPORT)).Select(v => (double)v).ToArray();
            this.currentScissor = new Vector4().fromArray(scissorParam);
            this.currentViewport = new Vector4().fromArray(viewportParam);
            this.emptyTextures = new JsObj<int, GLInt>();
            this.emptyTextures[gl.TEXTURE_2D] = createTexture(gl.TEXTURE_2D, gl.TEXTURE_2D, 1);
            this.emptyTextures[gl.TEXTURE_CUBE_MAP] = createTexture(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_CUBE_MAP_POSITIVE_X, 6);
            // init

            colorBuffer.setClear(0, 0, 0, 1);
            depthBuffer.setClear(1);
            stencilBuffer.setClear(0);

            enable(gl.DEPTH_TEST);
            depthBuffer.setFunc(LessEqualDepth);
            setFlipSided(false);
            setCullFace(CullFaceBack);
            enable(gl.CULL_FACE);
            //OpenTK旧版没有默认开启需要初始化的时候开启
            enable(gl.PROGRAM_POINT_SIZE);
            enable(gl.POINT_SPRITE);
            setBlending(NoBlending);
            this.equationToGL = new JsObj<int, int?>
                {
                    { AddEquation , gl.FUNC_ADD },
                    {SubtractEquation, gl.FUNC_SUBTRACT},
                    {ReverseSubtractEquation , gl.FUNC_REVERSE_SUBTRACT},
                    {MinEquation , gl.MIN},
                    {MaxEquation , gl.MAX }
                };
            this.factorToGL = new JsObj<int, int?>
                {
                    {ZeroFactor , gl.ZERO},
                    {OneFactor , gl.ONE},
                    {SrcColorFactor , gl.SRC_COLOR},
                    {SrcAlphaFactor , gl.SRC_ALPHA},
                    {SrcAlphaSaturateFactor , gl.SRC_ALPHA_SATURATE},
                    {DstColorFactor , gl.DST_COLOR},
                    {DstAlphaFactor , gl.DST_ALPHA},
                    {OneMinusSrcColorFactor , gl.ONE_MINUS_SRC_COLOR},
                    {OneMinusSrcAlphaFactor , gl.ONE_MINUS_SRC_ALPHA},
                    {OneMinusDstColorFactor , gl.ONE_MINUS_DST_COLOR},
                    { OneMinusDstAlphaFactor , gl.ONE_MINUS_DST_ALPHA}
            };
            this.buffers = new Buffers
            {
                color = colorBuffer,
                depth = depthBuffer,
                stencil = stencilBuffer
            };
        }
        public GLInt createTexture(int type, int target, int count)
        {
            var data = new byte[4]; // 4 is required to match default unpack alignment of 4.
            var texture = gl.createTexture();
            gl.bindTexture(type, texture.Value);
            gl.texParameteri(type, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
            gl.texParameteri(type, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

            for (var i = 0; i < count; i++)
            {

                gl.texImage2D(target + i, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);

            }
            return texture;
        }

        public void enable(int id)
        {

            if (enabledCapabilities[id] != true)
            {

                gl.enable(id);
                enabledCapabilities[id] = true;

            }

        }

        public void disable(int id)
        {

            if (enabledCapabilities[id] != false)
            {

                gl.disable(id);
                enabledCapabilities[id] = false;

            }

        }

        public bool bindFramebuffer(int target, GLInt framebuffer)
        {
            if (framebuffer == null)
                framebuffer = new GLInt { Value = 0 };
            if (currentBoundFramebuffers[target] != framebuffer)
            {

                gl.bindFramebuffer(target, framebuffer.Value);
                currentBoundFramebuffers[target] = framebuffer;

                if (isWebGL2)
                {

                    // gl.DRAW_FRAMEBUFFER is equivalent to gl.FRAMEBUFFER

                    if (target == gl.DRAW_FRAMEBUFFER)
                    {

                        currentBoundFramebuffers[gl.FRAMEBUFFER] = framebuffer;

                    }

                    if (target == gl.FRAMEBUFFER)
                    {

                        currentBoundFramebuffers[gl.DRAW_FRAMEBUFFER] = framebuffer;

                    }

                }

                return true;

            }

            return false;
        }

        public void drawBuffers(WebGLRenderTarget renderTarget, GLInt framebuffer)
        {

            var drawBuffers = defaultDrawbuffers;

            var needsUpdate = false;

            if (renderTarget != null)
            {

                drawBuffers = currentDrawbuffers.get(framebuffer);

                if (drawBuffers == null)
                {

                    drawBuffers = new JsArr<int>();
                    currentDrawbuffers.set(framebuffer, drawBuffers);

                }

                if (renderTarget is WebGLMultipleRenderTargets)
                {

                    var multipleRenderTarget = renderTarget as WebGLMultipleRenderTargets;
                    var textures = multipleRenderTarget.textures;

                    if (drawBuffers.length != textures.length || drawBuffers[0] != gl.COLOR_ATTACHMENT0)
                    {

                        for (int i = 0, il = textures.length; i < il; i++)
                        {

                            drawBuffers[i] = gl.COLOR_ATTACHMENT0 + i;

                        }

                        drawBuffers.length = textures.length;

                        needsUpdate = true;

                    }

                }
                else
                {

                    if (drawBuffers[0] != gl.COLOR_ATTACHMENT0)
                    {

                        drawBuffers[0] = gl.COLOR_ATTACHMENT0;

                        needsUpdate = true;

                    }

                }

            }
            else
            {

                if (drawBuffers[0] != gl.BACK)
                {

                    drawBuffers[0] = gl.BACK;

                    needsUpdate = true;

                }

            }

            if (needsUpdate)
            {

                if (capabilities.isWebGL2)
                {

                    gl.drawBuffers(drawBuffers.length, drawBuffers.ToArray());

                }
                else
                {

                    //extensions.get("WEBGL_draw_buffers").drawBuffersWEBGL(drawBuffers);

                }

            }


        }

        public bool useProgram(int program)
        {

            if (currentProgram != program)
            {

                gl.useProgram(program);

                currentProgram = program;

                return true;

            }

            return false;

        }
        public class EquationToGL
        {
            public int AddEquation;
            public int SubtractEquation;
            public int ReverseSubtractEquation;
            public int MinEquation;
            public int MaxEquation;

        }
        public class FactorToGL
        {
            public int ZeroFactor = gl.ZERO;
            public int OneFactor = gl.ONE;
            public int SrcColorFactor = gl.SRC_COLOR;
            public int SrcAlphaFactor = gl.SRC_ALPHA;
            public int SrcAlphaSaturateFactor = gl.SRC_ALPHA_SATURATE;
            public int DstColorFactor = gl.DST_COLOR;
            public int DstAlphaFactor = gl.DST_ALPHA;
            public int OneMinusSrcColorFactor = gl.ONE_MINUS_SRC_COLOR;
            public int OneMinusSrcAlphaFactor = gl.ONE_MINUS_SRC_ALPHA;
            public int OneMinusDstColorFactor = gl.ONE_MINUS_DST_COLOR;
            public int OneMinusDstAlphaFactor = gl.ONE_MINUS_DST_ALPHA;
        }
        public void setBlending(int? blending, int? blendEquation = null, int? blendSrc = null, int? blendDst = null, int? blendEquationAlpha = null, int? blendSrcAlpha = null, int? blendDstAlpha = null, bool premultipliedAlpha = false)
        {

            if (blending == NoBlending)
            {

                if (currentBlendingEnabled)
                {

                    disable(gl.BLEND);
                    currentBlendingEnabled = false;

                }

                return;

            }

            if (!currentBlendingEnabled)
            {

                enable(gl.BLEND);
                currentBlendingEnabled = true;

            }

            if (blending != CustomBlending)
            {

                if (blending != currentBlending || premultipliedAlpha != currentPremultipledAlpha)
                {

                    if (currentBlendEquation != AddEquation || currentBlendEquationAlpha != AddEquation)
                    {

                        gl.blendEquation(gl.FUNC_ADD);

                        currentBlendEquation = AddEquation;
                        currentBlendEquationAlpha = AddEquation;

                    }

                    if (premultipliedAlpha)
                    {

                        switch (blending)
                        {

                            case NormalBlending:
                                gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
                                break;

                            case AdditiveBlending:
                                gl.blendFunc(gl.ONE, gl.ONE);
                                break;

                            case SubtractiveBlending:
                                gl.blendFuncSeparate(gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE);
                                break;

                            case MultiplyBlending:
                                gl.blendFuncSeparate(gl.ZERO, gl.SRC_COLOR, gl.ZERO, gl.SRC_ALPHA);
                                break;

                            default:
                                console.error("THREE.WebGLState: Invalid blending: ", blending);
                                break;

                        }

                    }
                    else
                    {

                        switch (blending)
                        {

                            case NormalBlending:
                                gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
                                break;

                            case AdditiveBlending:
                                gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
                                break;

                            case SubtractiveBlending:
                                gl.blendFuncSeparate(gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE);
                                break;

                            case MultiplyBlending:
                                gl.blendFunc(gl.ZERO, gl.SRC_COLOR);
                                break;

                            default:
                                console.error("THREE.WebGLState: Invalid blending: ", blending);
                                break;

                        }

                    }

                    currentBlendSrc = null;
                    currentBlendDst = null;
                    currentBlendSrcAlpha = null;
                    currentBlendDstAlpha = null;

                    currentBlending = blending;
                    currentPremultipledAlpha = premultipliedAlpha;

                }

                return;

            }

            // custom blending

            blendEquationAlpha = blendEquationAlpha ?? blendEquation;
            blendSrcAlpha = blendSrcAlpha ?? blendSrc;
            blendDstAlpha = blendDstAlpha ?? blendDst;

            if (blendEquation != currentBlendEquation || blendEquationAlpha != currentBlendEquationAlpha)
            {

                gl.blendEquationSeparate(equationToGL[blendEquation.Value].Value, equationToGL[blendEquationAlpha.Value].Value);

                currentBlendEquation = blendEquation;
                currentBlendEquationAlpha = blendEquationAlpha;

            }

            if (blendSrc != currentBlendSrc || blendDst != currentBlendDst || blendSrcAlpha != currentBlendSrcAlpha || blendDstAlpha != currentBlendDstAlpha)
            {

                gl.blendFuncSeparate(factorToGL[blendSrc.Value].Value, factorToGL[blendDst.Value].Value, factorToGL[blendSrcAlpha.Value].Value, factorToGL[blendDstAlpha.Value].Value);

                currentBlendSrc = blendSrc;
                currentBlendDst = blendDst;
                currentBlendSrcAlpha = blendSrcAlpha;
                currentBlendDstAlpha = blendDstAlpha;

            }

            currentBlending = blending;
            currentPremultipledAlpha = false;

        }

        public void setMaterial(Material material, bool frontFaceCW)
        {
            if (material.side == DoubleSide)
                disable(gl.CULL_FACE);
            else enable(gl.CULL_FACE);

            var flipSided = (material.side == BackSide);
            if (frontFaceCW) flipSided = !flipSided;

            setFlipSided(flipSided);

            if (material.blending == NormalBlending && material.transparent == false)
                setBlending(NoBlending);
            else setBlending(material.blending, material.blendEquation, material.blendSrc, material.blendDst, material.blendEquationAlpha, material.blendSrcAlpha, material.blendDstAlpha, material.premultipliedAlpha);

            depthBuffer.setFunc(material.depthFunc);
            depthBuffer.setTest(material.depthTest);
            depthBuffer.setMask(material.depthWrite);
            colorBuffer.setMask(material.colorWrite);

            var stencilWrite = material.stencilWrite;
            stencilBuffer.setTest(stencilWrite);
            if (stencilWrite)
            {

                stencilBuffer.setMask(material.stencilWriteMask);
                stencilBuffer.setFunc(material.stencilFunc, material.stencilRef, material.stencilFuncMask);
                stencilBuffer.setOp(material.stencilFail, material.stencilZFail, material.stencilZPass);

            }

            setPolygonOffset(material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits);

            if (material.alphaToCoverage)
                enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
            else disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
        }

        //

        public void setFlipSided(bool flipSided)
        {

            if (currentFlipSided != flipSided)
            {

                if (flipSided)
                {

                    gl.frontFace(gl.CW);

                }
                else
                {

                    gl.frontFace(gl.CCW);

                }

                currentFlipSided = flipSided;

            }

        }

        public void setCullFace(int cullFace)
        {

            if (cullFace != CullFaceNone)
            {

                enable(gl.CULL_FACE);

                if (cullFace != currentCullFace)
                {

                    if (cullFace == CullFaceBack)
                    {

                        gl.cullFace(gl.BACK);

                    }
                    else if (cullFace == CullFaceFront)
                    {

                        gl.cullFace(gl.FRONT);

                    }
                    else
                    {

                        gl.cullFace(gl.FRONT_AND_BACK);

                    }

                }

            }
            else
            {

                disable(gl.CULL_FACE);

            }

            currentCullFace = cullFace;

        }

        public void setLineWidth(float width)
        {

            if (width != currentLineWidth)
            {

                if (lineWidthAvailable) gl.lineWidth(width);

                currentLineWidth = width;

            }

        }

        public void setPolygonOffset(bool polygonOffset, float factor = 0, float units = 0)
        {

            if (polygonOffset)
            {

                enable(gl.POLYGON_OFFSET_FILL);

                if (currentPolygonOffsetFactor != factor || currentPolygonOffsetUnits != units)
                {

                    gl.polygonOffset(factor, units);

                    currentPolygonOffsetFactor = factor;
                    currentPolygonOffsetUnits = units;

                }

            }
            else
            {

                disable(gl.POLYGON_OFFSET_FILL);

            }

        }

        public void setScissorTest(bool scissorTest)
        {

            if (scissorTest)
            {

                enable(gl.SCISSOR_TEST);

            }
            else
            {

                disable(gl.SCISSOR_TEST);

            }

        }

        // texture

        public void activeTexture(int? webglSlot)
        {

            if (webglSlot == null) webglSlot = gl.TEXTURE0 + maxTextures - 1;

            if (currentTextureSlot != webglSlot)
            {

                gl.activeTexture(webglSlot.Value);
                currentTextureSlot = webglSlot;

            }

        }
        public class BoundTexture
        {
            public int? type;
            public GLInt texture;
        }
        public void bindTexture(int webglType, GLInt webglTexture, int? webglSlot)
        {

            if (webglSlot == null)
            {

                if (currentTextureSlot == null || currentTextureSlot < 0)
                {

                    webglSlot = gl.TEXTURE0 + maxTextures - 1;

                }
                else
                {

                    webglSlot = currentTextureSlot;

                }

            }

            var boundTexture = currentBoundTextures[webglSlot.Value];

            if (boundTexture == null)
            {

                boundTexture = new BoundTexture { type = null, texture = null };
                currentBoundTextures[webglSlot.Value] = boundTexture;

            }

            if (boundTexture.type != webglType || boundTexture.texture != webglTexture)
            {

                if (currentTextureSlot != webglSlot)
                {

                    gl.activeTexture(webglSlot.Value);
                    currentTextureSlot = webglSlot;

                }

                gl.bindTexture(webglType, webglTexture?.Value ?? emptyTextures[webglType].Value);

                boundTexture.type = webglType;
                boundTexture.texture = webglTexture;

            }

        }

        public void unbindTexture()
        {

            var boundTexture = currentBoundTextures[currentTextureSlot.Value];

            if (boundTexture != null && boundTexture.type != null)
            {

                gl.bindTexture(boundTexture.type.Value, 0);

                boundTexture.type = null;
                boundTexture.texture = null;

            }

        }
        public void compressedTexImage2D(int target, Int32 level, int internalformat, Int32 width, Int32 height, Int32 border, Int32 imageSize, Image img)
        {

            try
            {
                var imgptr = img.startRead();
                gl.compressedTexImage2D(target, level, internalformat, width, height, border, imageSize, imgptr);
                img.endRead();
            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        public void compressedTexImage3D(int target, Int32 level, int internalformat, Int32 width, Int32 height, Int32 depth, Int32 border, Int32 imageSize, Image img)
        {

            try
            {
                var imgptr = img.startRead();
                gl.CompressedTexImage3D(target, level, internalformat, width, height, depth, border, imageSize, imgptr);
                img.endRead();
            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        public void texSubImage2D(int target, Int32 level, Int32 xoffset, Int32 yoffset, Int32 width, Int32 height, int format, int type, Image img)
        {
            try
            {

                var imgptr = img.startRead(type);
                gl.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, imgptr);
                img.endRead();


            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        public void texSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int width, int height, int depth, int format, int type, Image img)
        {
            try
            {
                var imgptr = img.startRead(type);
                gl.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, imgptr);
                img.endRead();
            }
            catch (Exception error)
            {
                console.error("THREE.WebGLState:", error.Message);
            }
        }

        public void compressedTexSubImage2D(int target, Int32 level, int xoffset, Int32 yoffset, Int32 width, Int32 height, Int32 format, Int32 imageSize, Image img)
        {

            try
            {
                var imgPtr = img.startRead(gl.UNSIGNED_BYTE);
                gl.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, imageSize, imgPtr);
                img.endRead();
            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);
            }
        }

        public void compressedTexSubImage3D(int target, Int32 level, int xoffset, Int32 yoffset, Int32 zoffset, Int32 width, Int32 height, Int32 depth, Int32 format, Int32 imageSize, Image img)
        {
            try
            {
                var imgPtr = img.startRead();
                gl.compressedTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, imgPtr);
                img.endRead();
            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        public void texStorage2D(int target, Int32 levels, Int32 internalformat, Int32 width, Int32 height)
        {
            try
            {

                gl.texStorage2D(target, levels, internalformat, width, height);

            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        public void texStorage3D(int target, Int32 levels, Int32 internalformat, Int32 width, Int32 height, Int32 depth)
        {

            try
            {

                gl.texStorage3D(target, levels, internalformat, width, height, depth);

            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        public void texImage2D(int target, Int32 level, int internalformat, Int32 width, Int32 height, Int32 border, int format, int type, Image img)
        {

            try
            {
                var imgptr = img?.startRead(type) ?? IntPtr.Zero;
                gl.texImage2D(target, level, internalformat, width, height, border, format, type, imgptr);
                img?.endRead();
            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        public void texImage3D(int target, Int32 level, int internalformat, Int32 width, Int32 height, Int32 depth, Int32 border, int format, int type, Image img)
        {

            try
            {
                var imgptr = img?.startRead(type) ?? IntPtr.Zero;
                gl.texImage3D(target, level, internalformat, width, height, depth, border, format, type, imgptr);
                img?.endRead();
            }
            catch (Exception error)
            {

                console.error("THREE.WebGLState:", error.Message);

            }

        }

        //

        public void scissor(Vector4 scissor)
        {

            if (!currentScissor.equals(scissor))
            {

                gl.scissor((int)scissor.x, (int)scissor.y, (int)scissor.z, (int)scissor.w);
                currentScissor.copy(scissor);
            }

        }

        public void viewport(Vector4 viewport)
        {

            if (!currentViewport.equals(viewport))
            {

                gl.viewport((int)viewport.x, (int)viewport.y, (int)viewport.z, (int)viewport.w);
                currentViewport.copy(viewport);

            }

        }

        public void updateUBOMapping(UniformsGroup uniformsGroup, int program)
        {

            var mapping = uboProgramMap.get(program);

            if (mapping == null)
            {

                mapping = new JsObj<UniformsGroup, int?>();

                uboProgramMap.set(program, mapping);

            }

            var blockIndex = mapping.get(uniformsGroup);

            if (blockIndex == null)
            {

                blockIndex = gl.getUniformBlockIndex(program, uniformsGroup.name);

                mapping.set(uniformsGroup, blockIndex);

            }

        }

        public void uniformBlockBinding(UniformsGroup uniformsGroup, int program)
        {

            var mapping = uboProgramMap.get(program);
            var blockIndex = mapping.get(uniformsGroup);

            if (uboBindings.get(program) != blockIndex)
            {

                // bind shader specific block index to global block point
                gl.uniformBlockBinding(program, blockIndex.Value, uniformsGroup.__bindingPointIndex.Value);

                uboBindings.set(program, blockIndex);

            }

        }

        //

        public void reset()
        {

            // reset state

            gl.disable(gl.BLEND);
            gl.disable(gl.CULL_FACE);
            gl.disable(gl.DEPTH_TEST);
            gl.disable(gl.POLYGON_OFFSET_FILL);
            gl.disable(gl.SCISSOR_TEST);
            gl.disable(gl.STENCIL_TEST);
            gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);

            gl.blendEquation(gl.FUNC_ADD);
            gl.blendFunc(gl.ONE, gl.ZERO);
            gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO);

            gl.colorMask(true, true, true, true);
            gl.clearColor(0, 0, 0, 0);

            gl.depthMask(true);
            gl.depthFunc(gl.LESS);
            gl.clearDepth(1);

            gl.stencilMask(0xffffffff);
            gl.stencilFunc(gl.ALWAYS, 0, 0xffffffff);
            gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
            gl.clearStencil(0);

            gl.cullFace(gl.BACK);
            gl.frontFace(gl.CCW);

            gl.polygonOffset(0, 0);

            gl.activeTexture(gl.TEXTURE0);

            gl.bindFramebuffer(gl.FRAMEBUFFER, 0);

            if (isWebGL2)
            {

                gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, 0);
                gl.bindFramebuffer(gl.READ_FRAMEBUFFER, 0);

            }

            gl.useProgram(0);

            gl.lineWidth(1);

            gl.scissor(0, 0, GLUtils.Canvas.Width, GLUtils.Canvas.Height);
            gl.viewport(0, 0, GLUtils.Canvas.Width, GLUtils.Canvas.Height);

            // reset internals

            enabledCapabilities = new JsObj<int, bool?>();

            currentTextureSlot = null;
            currentBoundTextures = new JsObj<int, BoundTexture>();

            currentBoundFramebuffers = new JsObj<int, GLInt>();
            currentDrawbuffers = new JsObj<GLInt, JsArr<int>>();
            defaultDrawbuffers = new JsArr<int>();

            currentProgram = null;

            currentBlendingEnabled = false;
            currentBlending = null;
            currentBlendEquation = null;
            currentBlendSrc = null;
            currentBlendDst = null;
            currentBlendEquationAlpha = null;
            currentBlendSrcAlpha = null;
            currentBlendDstAlpha = null;
            currentPremultipledAlpha = false;

            currentFlipSided = null;
            currentCullFace = null;

            currentLineWidth = null;

            currentPolygonOffsetFactor = null;
            currentPolygonOffsetUnits = null;

            currentScissor.set(0, 0, GLUtils.Canvas.Width, GLUtils.Canvas.Height);
            currentViewport.set(0, 0, GLUtils.Canvas.Width, GLUtils.Canvas.Height);

            colorBuffer.reset();
            depthBuffer.reset();
            stencilBuffer.reset();

        }
    }
}
